﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Mvc;
using System.Web.Services.Description;
using Castle.Core.Resource;
using Castle.Windsor;
using Castle.Windsor.Configuration.Interpreters;
using FaceSharp.Api.Common;
using FaceSharp.Api.GraphApi;
using FaceSharp.Api;
using FaceSharp.Api.Types;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Security.Cryptography;
using System.Text;
using Types = FaceSharp.Api.Types;



namespace FaceSharp.MVC.Filters
{
    public class FacebookGraphApiFilter : ActionFilterAttribute
    {
        private const string FacebookCookiePrefix = "facesharp_";
        private bool _forceLogin = true;

        /// <summary>
        /// From the Facebook Developers App
        /// </summary>
        public string ApplicationId { get; set; }

        /// <summary>
        /// From the Facebook Developers App
        /// </summary>
        public string ApplicationSecret { get; set; }

        /// <summary>
        /// Extended Permissions your app needs, in a comma separated list
        /// https://developers.facebook.com/docs/authentication/permissions/
        /// </summary>
        public string ExtendedPermissions { get; set; }

        /// <summary>
        /// Used for redirect_uri : The redirect_uri must be within the same domain as the Site URL you specify in Web site tab of the Developer App:
        /// https://developers.facebook.com/docs/reference/dialogs/oauth/
        /// </summary>
        public string ApplicationUrl { get; set; }

        /// <summary>
        /// If you set to true it will set the redirect_uri to the URI where the user came from on your site. (Default value is False)
        /// https://developers.facebook.com/docs/reference/dialogs/oauth/
        /// </summary>
        public bool RedirectToOriginatingUrl { get; set; }

        /// <summary>
        /// Optional, default is token. The requested response: an access token (token), an authorization code (code), or both (code token).
        /// http://developers.facebook.com/docs/reference/dialogs/oauth/
        /// </summary>
        public string ResponseType { get; set; }

        /// <summary>
        /// Set to true if on a canvas page, will use javascript redirect. (Default value is false)
        /// </summary>
        public bool CanvasRedirect { get; set; }

        /// <summary>
        /// If using CanvasRedirect adds this string on your app url.
        /// </summary>
        public string CanvasRedirectPath { get; set; }

        /// <summary>
        /// You may not always want to force the user to log on to a page. (Default value is True)
        /// </summary>
        public bool ForceLogin
        {
            get { return _forceLogin; }
            set { _forceLogin = value; }
        }

        /// <summary>
        /// Setting for auth type: page, popup, iframe, touch, wap
        /// http://developers.facebook.com/docs/reference/dialogs/#display
        /// </summary>
        public string Display { get; set; }

        private readonly IFacebookCore _facebookCore;

        public FacebookGraphApiFilter()
        {
            RedirectToOriginatingUrl = false;
            var container = new WindsorContainer(new XmlInterpreter(new ConfigResource("castle")));
            _facebookCore = (IFacebookCore)container.Resolve(typeof(IFacebookCore));

            ApplicationId = _facebookCore.ApplicationId;
            ApplicationSecret = _facebookCore.ApplicationSecret;
            ApplicationUrl = _facebookCore.ApplicationUrl;
            ExtendedPermissions = _facebookCore.ExtendedPermissions;
        }

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            User user;
            ValidateContext(filterContext.HttpContext, out user);

            //We don't have a valid session or code, we need to prompt the user to authorize by redirecting them to the OAuth URL
            if (user == null && ForceLogin == true)
            {
                Trace.Write("Force User to Authorize Application", "FacebookGraphApiFilter.OnActionExecuting");

                var redirectUrl = GetRedirectUri(filterContext.HttpContext, _facebookCore, RedirectToOriginatingUrl);
                if (CanvasRedirect && !string.IsNullOrWhiteSpace(CanvasRedirectPath))
                {
                   redirectUrl += CanvasRedirectPath;
                }

                ShowOAuthDialog(
                    filterContext,
                    _facebookCore.ApplicationId,
                    redirectUrl,
                    responseType: ResponseType,
                    scope: _facebookCore.ExtendedPermissions,
                    javascriptFragmentRedirect: CanvasRedirect);
            }
        }

        internal void ValidateContext(HttpContextBase httpContext, out User user)
        {
            Trace.Write("Begin", "FacebookGraphApiFilter.ValidateContext");
            // using OAuth 2.0 for Canvas - http://developers.facebook.com/docs/authentication/canvas

            Trace.Write("ForceLogin Value:" + ForceLogin, "FacebookGraphApiFilter.ValidateContext");
            Trace.Write("RedirectToOriginatingUrl Value:" + RedirectToOriginatingUrl, "FacebookGraphApiFilter.ValidateContext");

            Types.SignedRequest signedRequest = null;
            user = null;

            var redirectUri = GetRedirectUri(httpContext, _facebookCore, RedirectToOriginatingUrl);
           
            var validationProgress = new ValidationProgress()
            {
                AuthKey = ValidationProgress.ValidationState.UnChecked
            };

            var signedRequestAuthorizationPayload = httpContext.Request["signed_request"];
            var codeAuthorizationParameter = httpContext.Request["code"];
            var cookieAuthorizationPayload = httpContext.Request.Cookies[FacebookCookiePrefix + ApplicationId];

            //
            // 1. SIGNED_REQUEST Request Parameter Check
            //
            if (!String.IsNullOrWhiteSpace(signedRequestAuthorizationPayload))
            {
                // check if the posted signed_request is present and if we can get the current user
                Trace.Write("Signed Request Parameter Present", "FacebookGraphApiFilter.ValidateContext");
                signedRequest = GetSignedRequest(signedRequestAuthorizationPayload, ApplicationSecret);
                validationProgress = ValidateSignedRequest(ApplicationId, ApplicationSecret, signedRequest, out user, redirectUri);
            }

            //
            // 2. CODE Request Parameter Check
            //
            if (
                // check if the posted code is present and if we can get the current user
                validationProgress.AuthKey != ValidationProgress.ValidationState.Certified &&
                !String.IsNullOrWhiteSpace(codeAuthorizationParameter)
                )
            {
                Trace.Write("Code Parameter Present", "FacebookGraphApiFilter.ValidateContext");
                signedRequest = new SignedRequest() { Code = codeAuthorizationParameter };
                validationProgress = ValidateSignedRequest(ApplicationId, ApplicationSecret, signedRequest, out user, redirectUri);
            }

            //
            // 3. COOKIE Check
            //
            if (
                // if we still don't have an authkey check if we got one from a cookie
                validationProgress.AuthKey != ValidationProgress.ValidationState.Certified &&
                cookieAuthorizationPayload != null &&
                !String.IsNullOrWhiteSpace(cookieAuthorizationPayload.Value)
                )
            {
                Trace.Write("Cookie Parameter Present", "FacebookGraphApiFilter.ValidateContext");
                signedRequest = GetSignedRequest(cookieAuthorizationPayload.Value, ApplicationSecret);
                validationProgress = ValidateSignedRequest(ApplicationId, ApplicationSecret, signedRequest, out user, redirectUri);
            }

            // perhaps the developer implementing the library stored the auth key and we can access it from facebook core
            if (
                validationProgress.AuthKey != ValidationProgress.ValidationState.Certified &&
                !string.IsNullOrWhiteSpace(_facebookCore.AccessToken)
                )
            {
                Trace.Write("IFacebookCore AuthKey Present", "FacebookGraphApiFilter.ValidateContext");
                signedRequest = new SignedRequest() {OAuthToken = _facebookCore.AccessToken};
                validationProgress = ValidateSignedRequest(ApplicationId, ApplicationSecret, signedRequest, out user, redirectUri);
            }

            // perhaps the developer implementing the library stored the code and we can access it from facebook core
            if (
                validationProgress.AuthKey != ValidationProgress.ValidationState.Certified &&
                !string.IsNullOrWhiteSpace(_facebookCore.Code)
                )
            {
                Trace.Write("IFacebookCore Code Present", "FacebookGraphApiFilter.ValidateContext");
                signedRequest = new SignedRequest() {Code = _facebookCore.Code};
                validationProgress = ValidateSignedRequest(ApplicationId, ApplicationSecret, signedRequest, out user, redirectUri);
            }

            // always set signed request if not null
            if (signedRequest != null && !string.IsNullOrWhiteSpace(signedRequest.Algorithm))
            {
                _facebookCore.SignedRequest = signedRequest;
            }

            var cookie = new HttpCookie(FacebookCookiePrefix + ApplicationId);
            // now that we may have an access key
            if (
                validationProgress.AuthKey == ValidationProgress.ValidationState.Certified &&
                signedRequest != null &&
                user != null
                )
            {
                Trace.Write("Access Token Present", "FacebookGraphApiFilter.ValidateContext");
                Trace.Write("access_token: " + signedRequest.OAuthToken, "FacebookGraphApiFilter.ValidateContext");
                Trace.Write("uid: " + user.Id, "FacebookGraphApiFilter.ValidateContext");

                _facebookCore.AccessToken = signedRequest.OAuthToken;
                _facebookCore.AccessTokenExpirationDate = !signedRequest.Expires.HasValue
                                                              ? DateTime.MinValue
                                                              : ConversionUtil.UnixTimeStampToDateTime( signedRequest.Expires.Value);
                _facebookCore.Code = signedRequest.Code;
                _facebookCore.CurrentUser = user;

                cookie.Value = CreateFacebookCookiePayload(signedRequest, ApplicationSecret);
            }
            else
            {
                cookie.Value = string.Empty;
            }

            httpContext.Response.Cookies.Add(cookie);

            // Add a header to allow us to set cookies from within an IFrame
            httpContext.Response.AddHeader("P3P", "CP=\"CAO PSA OUR\"");

            Trace.Write("End", "FacebookGraphApiFilter.ValidateContext");
        }

        public struct ValidationProgress
        {
            public enum ValidationState
            {
                UnChecked,
                Present,
                Valid,
                Certified
            }
            public ValidationState AuthKey { get; set; }
        }

        /// <summary>
        /// http://developers.facebook.com/docs/authentication/signed_request/
        /// </summary>
        /// <returns></returns>
        public Types.SignedRequest GetSignedRequest(string authPayload, string applicationSecret)
        {
            Trace.Write("Begin", "FacebookGraphApiFilter.GetSignedRequest");
            Types.SignedRequest signedRequest = null;
            if (authPayload != null && authPayload.Contains("."))
            {
                Trace.Write("Parameter Present", "FacebookGraphApiFilter.GetSignedRequest");

                var signature = authPayload.Split('.')[0];
                var encodedJsonObject = authPayload.Split('.')[1];

                if (ValidatePayloadSignature(applicationSecret, signature, encodedJsonObject))
                {
                    var decodedPayloadString = DecodePayload(encodedJsonObject);
                    signedRequest = JsonConvert.DeserializeObject<Types.SignedRequest>(decodedPayloadString);
                }
            }
            Trace.Write("End", "FacebookGraphApiFilter.GetSignedRequest");
            return signedRequest;
        }

        public bool ValidatePayloadSignature(string secret, string signature, string payload)
        {
            Trace.Write("Begin", "FacebookGraphApiFilter.ValidatePayloadSignature");
            signature = signature.Replace("=", string.Empty).Replace('-', '+').Replace('_', '/');
            var payloadSignature = GeneratePayloadSignature(secret, payload);
            var returnVal = signature == payloadSignature;
            Trace.Write("IsValid:" + returnVal, "FacebookGraphApiFilter.ValidatePayloadSignature");
            Trace.Write("End", "FacebookGraphApiFilter.ValidatePayloadSignature");
            return returnVal;
        }

        public static string GeneratePayloadSignature(string secret, string payload)
        {
            Trace.Write("Begin", "FacebookGraphApiFilter.GeneratePayloadSignature");
            var encoding = new UTF8Encoding();
            var hmacsha256 = new HMACSHA256(encoding.GetBytes(secret));
            var computedHash = hmacsha256.ComputeHash(encoding.GetBytes(payload));
            var payloadSignature = Convert.ToBase64String(computedHash).Replace("=", string.Empty);
            Trace.Write("End", "FacebookGraphApiFilter.GeneratePayloadSignature");
            return payloadSignature;
        }

        public string DecodePayload(string payload)
        {
            Trace.Write("Begin", "FacebookGraphApiFilter.DecodePayload");
            var encoding = new UTF8Encoding();
            var decodedJson = payload.Replace("=", string.Empty).Replace('-', '+').Replace('_', '/');
            var base64JsonArray = Convert.FromBase64String(decodedJson.PadRight(decodedJson.Length + (4 - decodedJson.Length % 4) % 4, '='));
            var json = encoding.GetString(base64JsonArray);
            Trace.Write("Payload:" + json, "FacebookGraphApiFilter.DecodePayload");
            Trace.Write("End", "FacebookGraphApiFilter.DecodePayload");
            return json;
        }

        public string EncodePayload(string payload)
        {
            Trace.Write("Begin", "FacebookGraphApiFilter.EncodePayload");
            var encoding = new UTF8Encoding();
            var utf8Encoded = encoding.GetBytes(payload);
            var toBase64 = Convert.ToBase64String(utf8Encoded);
            var encodedJson = toBase64.TrimEnd('=').Replace('+', '-').Replace('/', '_');
            Trace.Write("End", "FacebookGraphApiFilter.EncodePayload");
            return encodedJson;
        }

        public string CreateFacebookCookiePayload(SignedRequest signedRequest, string applicationSecret)
        {
            Trace.Write("Begin", "FacebookGraphApiFilter.CreateFacebookCookiePayload");
            var jsonString = JsonConvert.SerializeObject(signedRequest);
            var payload = EncodePayload(jsonString);
            var payloadSignature = GeneratePayloadSignature(applicationSecret, payload).TrimEnd('=').Replace('+', '-').Replace('/', '_');
            var combinedString = payloadSignature + "." + payload;
            Trace.Write("End", "FacebookGraphApiFilter.CreateFacebookCookiePayload");
            return combinedString;
        }

        public void GetSessionFromCode(string applicationId, string applicationSecret, string code, out string accessToken, out long? expires, string redirectUri = null)
        {
            Trace.Write("Begin", "FacebookGraphApiFilter.GetSessionFromCode");

            accessToken = null;
            expires = null;

            var client = new RestSharp.RestClient("https://graph.facebook.com/");
            var request = new RestSharp.RestRequest("oauth/access_token", RestSharp.Method.GET);

            Trace.Write("client_id :" + ApplicationId, "FacebookGraphApiFilter.GetSessionFromCode");
            Trace.Write("client_secret : " + ApplicationSecret, "FacebookGraphApiFilter.GetSessionFromCode");
            Trace.Write("code :" + code, "FacebookGraphApiFilter.GetSessionFromCode");

            request.AddParameter("client_id", ApplicationId);
            request.AddParameter("client_secret", ApplicationSecret);
            request.AddParameter("code", code);

            if (!string.IsNullOrWhiteSpace(redirectUri))
            {
                Trace.Write("redirect_uri :" + redirectUri, "FacebookGraphApiFilter.GetSessionFromCode");
                request.AddParameter("redirect_uri", redirectUri);
            }

            Trace.Write(String.Format("Getting new access token from : https://graph.facebook.com/oauth/access_token?client_id={0}&client_secret={1}&code={2}&redirect_uri={3}", ApplicationId, ApplicationSecret, code, redirectUri));

            var response = client.Execute(request);
            if (response.StatusCode == HttpStatusCode.OK)
            {
                Trace.Write(response.Content);
                accessToken = System.Web.HttpUtility.ParseQueryString(response.Content).Get("access_token");
                expires = int.Parse(System.Web.HttpUtility.ParseQueryString(response.Content).Get("expires"));
            }
            else
            {
                Trace.TraceWarning("Error" + response.Content);
            }
            Trace.Write("End", "FacebookGraphApiFilter.GetSessionFromCode");
        }

        public ValidationProgress ValidateSignedRequest(string applicationId, string applicationSecret, Types.SignedRequest signedRequest, out User user, string redirectUri = null)
        {
            Trace.Write("Begin", "FacebookGraphApiFilter.ValidateSignedRequest");
            user = null;
            var validationProgress = new ValidationProgress()
            {
                AuthKey = ValidationProgress.ValidationState.UnChecked
            };
            if (signedRequest == null) return validationProgress;
            // should we ask for a new code?
            if (
                (
                String.IsNullOrWhiteSpace(signedRequest.OAuthToken)
                ) 
                ||
                (
                    !String.IsNullOrWhiteSpace(signedRequest.Code)
                    &&
                    (
                        signedRequest.Expires.HasValue && 
                        (DateTime.Now.GetUnixEpoch() > signedRequest.Expires.Value)
                    )
                ) 
            )
            {
                Trace.Write("Requesting new AuthToken from Code", "FacebookGraphApiFilter.ValidateSignedRequest");

                string oauthToken;
                long? expires;

                GetSessionFromCode(applicationId, applicationSecret, signedRequest.Code, out oauthToken, out expires, redirectUri);

                if (oauthToken != null)
                {
                    Trace.Write("Received new AuthToken using Code, attempting to get current user.", "FacebookGraphApiFilter.ValidateSignedRequest");
                    var fbApi = new FacebookApi(oauthToken);
                    var currentUser = fbApi.User.GetCurrentUserNoCore();

                    Trace.Write("Checking current user Begin");
                    if (currentUser != null && currentUser.Id > 0)
                    {
                        Trace.Write("Current User is Valid");
                        Trace.Write("AccessToken (From Code): " + oauthToken);
                        Trace.Write("Expires (From Code): " + expires);
                        signedRequest.OAuthToken = oauthToken;
                        signedRequest.Expires = expires;

                        user = currentUser;
                        validationProgress.AuthKey = ValidationProgress.ValidationState.Certified;
                    }
                    Trace.Write("Checking current user End");
                }
            }

            // OAuthToken present, can we get a user?
            // not checking expires date now : ConversionUtil.UnixTimeStampToDateTime(signedRequest.Expires.Value) <= DateTime.Now &&
            if (!String.IsNullOrWhiteSpace(signedRequest.OAuthToken) && validationProgress.AuthKey != ValidationProgress.ValidationState.Certified)
            {
                Trace.Write("AuthToken Present, attempting to get current user.", "FacebookGraphApiFilter.ValidateSignedRequest");
                var accessToken = signedRequest.OAuthToken;
                var fbApi = new FacebookApi(accessToken);
                var currentUser = fbApi.User.GetCurrentUserNoCore();
                if (currentUser != null && currentUser.Id > 0)
                {
                    user = currentUser;
                    validationProgress.AuthKey = ValidationProgress.ValidationState.Certified;
                }
            }
            Trace.Write("End", "FacebookGraphApiFilter.ValidateSignedRequest");
            return validationProgress;
        }

        /// <summary>
        /// Implementation of the OAuthDialog
        /// http://developers.facebook.com/docs/reference/dialogs/oauth/
        /// Prompt the user to grant permissions to your application. Use this dialog whenever your application needs to request additional permissions from your users.
        /// </summary>
        /// <param name="filterContext">The current filter context</param>
        /// <param name="clientId">Your application's identifier. This is called client_id instead of app_id for this particular method to be compliant with the OAuth 2.0 specification. Required, but automatically specified by most SDKs.</param>
        /// <param name="redirectUri">The URL to redirect to after the user clicks a button on the dialog. Required, but automatically specified by most SDKs.</param>
        /// <param name="scope">Optional. A comma-delimited list of permissions.</param>
        /// <param name="state">Optional. An opaque string used to maintain application state between the request and callback. When Facebook redirects the user back to your redirect_uri, this value will be included unchanged in the response.</param>
        /// <param name="responseType">Optional, default is token. The requested response: an access token (token), an authorization code (code), or both (code token).</param>
        /// <param name="display">The display mode in which to render the dialog. The default is page on the www subdomain and wap on the m subdomain. This is automatically specified by most SDKs.</param>
        /// <param name="javascriptFragmentRedirect">Navigates to the top window of user's browser. (Must set true for canvas apps)</param>
        private void ShowOAuthDialog(ActionExecutingContext filterContext, string clientId, string redirectUri, string scope = null, string state = null, string responseType = null, string display = null, bool javascriptFragmentRedirect = false)
        {
            Trace.Write("Begin", "FacebookGraphApiFilter.ShowOAuthDialog");

            var scopeArgument = String.IsNullOrWhiteSpace(scope) ? "" : "&scope=" + scope;
            var stateArgument = String.IsNullOrWhiteSpace(state) ? "" : "&state=" + state;
            var displayArgument = String.IsNullOrWhiteSpace(display) ? "" : "&display=" + display;
            var responseTypeArgument = String.IsNullOrWhiteSpace(responseType) ? "" : "&response_type=" + responseType;

            var authUrl = String.Format("https://www.facebook.com/dialog/oauth?client_id={0}&redirect_uri={1}{2}{3}{4}{5}", 
                clientId, 
                redirectUri,
                scopeArgument, 
                stateArgument, 
                responseTypeArgument,
                displayArgument);

            if(javascriptFragmentRedirect)
            {
                filterContext.HttpContext.Response.Write(String.Format("<script language='javascript'>window.open('{0}','_parent')</script>", authUrl));
            }
            else
            {
                filterContext.HttpContext.Response.Redirect(authUrl);
            }

            Trace.Write("End", "FacebookGraphApiFilter.PromptUserToAuthorizeApplication");
            filterContext.HttpContext.Response.End();
            filterContext.Result = new EmptyResult();
        }

        public string GetRedirectUri(HttpContextBase context, IFacebookCore facebookCore, bool redirectToOriginatingUrl)
        {
            if (context.Request == null || context.Request.Url == null) return facebookCore.ApplicationUrl;

            var originatingUrl = String.Format("{0}://{1}{2}", context.Request.Url.Scheme, context.Request.ServerVariables["HTTP_HOST"], context.Request.Url.AbsolutePath);
            return redirectToOriginatingUrl ? originatingUrl : facebookCore.ApplicationUrl;
        }

    }
}